اكتشف الاختبار القائم على الخصائص باستخدام مكتبة Hypothesis في Python. تجاوز الاختبارات القائمة على الأمثلة للعثور على الحالات الطرفية وبناء برامج أكثر قوة وموثوقية.
أبعد من اختبارات الوحدة: نظرة متعمقة على الاختبار القائم على الخصائص باستخدام Python Hypothesis
في عالم تطوير البرمجيات، يعد الاختبار حجر الزاوية للجودة. لعقود من الزمان، كان النموذج المهيمن هو الاختبار القائم على الأمثلة. نقوم بصياغة المدخلات بدقة، وتحديد المخرجات المتوقعة، وكتابة التأكيدات للتحقق من أن التعليمات البرمجية الخاصة بنا تتصرف على النحو المخطط له. هذا النهج، الموجود في أطر العمل مثل unittest
و pytest
، قوي وأساسي. ولكن ماذا لو أخبرتك أن هناك نهجًا تكميليًا يمكنه الكشف عن الأخطاء التي لم تفكر حتى في البحث عنها؟
مرحبًا بك في عالم الاختبار القائم على الخصائص، وهو نموذج يحول التركيز من اختبار أمثلة معينة إلى التحقق من الخصائص العامة للتعليمات البرمجية الخاصة بك. وفي نظام Python البيئي، فإن البطل بلا منازع لهذا النهج هو مكتبة تسمى Hypothesis.
سوف يأخذك هذا الدليل الشامل من مبتدئ تمامًا إلى ممارس واثق للاختبار القائم على الخصائص باستخدام Hypothesis. سوف نستكشف المفاهيم الأساسية، ونتعمق في الأمثلة العملية، ونتعلم كيفية دمج هذه الأداة القوية في سير عمل التطوير اليومي لبناء برامج أكثر قوة وموثوقية ومقاومة للأخطاء.
ما هو الاختبار القائم على الخصائص؟ تحول في العقلية
لفهم Hypothesis، نحتاج أولاً إلى فهم الفكرة الأساسية للاختبار القائم على الخصائص. دعنا نقارنها بالاختبار التقليدي القائم على الأمثلة الذي نعرفه جميعًا.
الاختبار القائم على الأمثلة: المسار المألوف
تخيل أنك كتبت دالة فرز مخصصة، my_sort()
. باستخدام الاختبار القائم على الأمثلة، ستكون عملية تفكيرك هي:
- "دعنا نختبرها بقائمة بسيطة مرتبة." ->
assert my_sort([1, 2, 3]) == [1, 2, 3]
- "ماذا عن قائمة مرتبة عكسيًا؟" ->
assert my_sort([3, 2, 1]) == [1, 2, 3]
- "ماذا عن قائمة فارغة؟" ->
assert my_sort([]) == []
- "قائمة بها تكرارات؟" ->
assert my_sort([5, 1, 5, 2]) == [1, 2, 5, 5]
- "وقائمة بأرقام سالبة؟" ->
assert my_sort([-1, -5, 0]) == [-5, -1, 0]
هذا فعال، ولكنه يتمتع بحد أساسي: أنت لا تختبر سوى الحالات التي يمكنك التفكير فيها. الاختبارات الخاصة بك جيدة فقط مثل خيالك. قد تفوتك الحالات الطرفية التي تتضمن أعدادًا كبيرة جدًا، وأخطاء في الفاصلة العائمة، وأحرف unicode معينة، أو مجموعات معقدة من البيانات التي تؤدي إلى سلوك غير متوقع.
الاختبار القائم على الخصائص: التفكير في الثوابت
يعكس الاختبار القائم على الخصائص السيناريو. بدلاً من توفير أمثلة محددة، فإنك تحدد الخصائص، أو الثوابت، لوظيفتك - القواعد التي يجب أن تكون صحيحة لأي إدخال صالح. بالنسبة لوظيفة my_sort()
الخاصة بنا، قد تكون هذه الخصائص:
- المخرجات مرتبة: لأي قائمة من الأرقام، يكون كل عنصر في قائمة الإخراج أقل من أو يساوي العنصر الذي يليه.
- المخرجات تحتوي على نفس عناصر الإدخال: القائمة المصنفة هي مجرد تبديل للقائمة الأصلية؛ لا تتم إضافة أو فقدان أي عناصر.
- الوظيفة ذاتية التكرار: يجب ألا يؤدي فرز قائمة مرتبة بالفعل إلى تغييرها. أي أن
my_sort(my_sort(some_list)) == my_sort(some_list)
.
باستخدام هذا النهج، فإنك لا تكتب بيانات الاختبار. أنت تكتب القواعد. ثم تسمح لإطار عمل، مثل Hypothesis، بإنشاء المئات أو الآلاف من المدخلات العشوائية والمتنوعة، والتي غالبًا ما تكون خبيثة، لمحاولة إثبات خطأ خصائصك. إذا وجد إدخالًا يكسر خاصية، فقد وجد خطأً.
تقديم Hypothesis: مولد بيانات الاختبار الآلي الخاص بك
Hypothesis هي مكتبة الاختبار القائمة على الخصائص المميزة لـ Python. يأخذ الخصائص التي تحددها ويقوم بالعمل الشاق المتمثل في إنشاء بيانات الاختبار لتحديها. إنها ليست مجرد مولد بيانات عشوائي؛ إنها أداة ذكية وقوية مصممة للعثور على الأخطاء بكفاءة.
الميزات الرئيسية لـ Hypothesis
- إنشاء حالة الاختبار التلقائي: يمكنك تحديد *شكل* البيانات التي تحتاجها (على سبيل المثال، "قائمة بالأعداد الصحيحة"، "سلسلة تحتوي على أحرف فقط"، "تاريخ ووقت في المستقبل")، ويقوم Hypothesis بإنشاء مجموعة واسعة من الأمثلة التي تتوافق مع هذا الشكل.
- التقلص الذكي: هذه هي الميزة السحرية. عندما يجد Hypothesis حالة اختبار فاشلة (على سبيل المثال، قائمة تضم 50 عددًا مركبًا تؤدي إلى تعطل دالة الفرز الخاصة بك)، فإنه لا يقتصر على الإبلاغ عن تلك القائمة الضخمة. بل يبسّط المدخلات بذكاء وتلقائيًا للعثور على أصغر مثال ممكن لا يزال يتسبب في الفشل. بدلاً من قائمة مكونة من 50 عنصرًا، قد يذكر أن الفشل يحدث مع
[inf, nan]
فقط. وهذا يجعل التصحيح سريعًا وفعالاً بشكل لا يصدق. - التكامل السلس: يتكامل Hypothesis بشكل مثالي مع أطر الاختبار الشائعة مثل
pytest
وunittest
. يمكنك إضافة اختبارات قائمة على الخصائص جنبًا إلى جنب مع اختباراتك القائمة على الأمثلة الحالية دون تغيير سير عملك. - مكتبة غنية من الاستراتيجيات: تأتي مع مجموعة كبيرة من "الاستراتيجيات" المضمنة لتوليد كل شيء بدءًا من الأعداد الصحيحة والسلاسل البسيطة إلى هياكل البيانات المعقدة والمتداخلة، وتواريخ وأوقات إدراك المنطقة الزمنية، وحتى مصفوفات NumPy.
- الاختبار ذو الحالة: بالنسبة للأنظمة الأكثر تعقيدًا، يمكن لـ Hypothesis اختبار تسلسلات الإجراءات للعثور على الأخطاء في انتقالات الحالة، وهو أمر يصعب تحقيقه بشكل سيئ مع الاختبار القائم على الأمثلة.
البدء: أول اختبار Hypothesis لك
دعنا نوسخ أيدينا. أفضل طريقة لفهم Hypothesis هي رؤيتها في العمل.
التثبيت
أولاً، ستحتاج إلى تثبيت Hypothesis وأداة تشغيل الاختبار التي تختارها (سنستخدم pytest
). الأمر بهذه البساطة:
pip install pytest hypothesis
مثال بسيط: دالة القيمة المطلقة
دعنا نفكر في دالة بسيطة من المفترض أن تحسب القيمة المطلقة لعدد ما. قد تبدو عملية التنفيذ المعيبة قليلاً هكذا:
# في ملف اسمه `my_math.py` def custom_abs(x): """تنفيذ مخصص لوظيفة القيمة المطلقة.""" if x < 0: return -x return x
الآن، دعنا نكتب ملف اختبار، test_my_math.py
. أولاً، النهج التقليدي pytest
:
# test_my_math.py (قائم على الأمثلة) def test_abs_positive(): assert custom_abs(5) == 5 def test_abs_negative(): assert custom_abs(-5) == 5 def test_abs_zero(): assert custom_abs(0) == 0
تنجح هذه الاختبارات. تبدو دالتنا صحيحة بناءً على هذه الأمثلة. ولكن الآن، دعنا نكتب اختبارًا قائمًا على الخصائص باستخدام Hypothesis. ما هي الخاصية الأساسية لوظيفة القيمة المطلقة؟ يجب ألا تكون النتيجة سالبة أبدًا.
# test_my_math.py (قائم على الخصائص باستخدام Hypothesis) from hypothesis import given from hypothesis import strategies as st from my_math import custom_abs @given(st.integers()) def test_abs_property_is_non_negative(x): """الخاصية: القيمة المطلقة لأي عدد صحيح تكون دائمًا >= 0.""" assert custom_abs(x) >= 0
دعنا نحلل هذا الأمر:
from hypothesis import given, strategies as st
: نستورد المكونات الضرورية.given
هو مزخرف يحول دالة اختبار عادية إلى اختبار قائم على الخصائص.strategies
هي الوحدة التي نجد فيها مولدات البيانات الخاصة بنا.@given(st.integers())
: هذا هو جوهر الاختبار. يخبر المزخرف@given
Hypothesis بتشغيل دالة الاختبار هذه عدة مرات. لكل تشغيل، سيقوم بإنشاء قيمة باستخدام الإستراتيجية المقدمة،st.integers()
، وتمريرها كوسيطةx
إلى دالة الاختبار الخاصة بنا.assert custom_abs(x) >= 0
: هذه هي خاصتنا. نؤكد أنه مهما كان العدد الصحيحx
الذي يخيله Hypothesis، يجب أن تكون نتيجة دالتنا أكبر من أو تساوي الصفر.
عند تشغيل هذا الأمر باستخدام pytest
، فمن المحتمل أن ينجح الأمر في العديد من القيم. سيحاول Hypothesis 0، -1، 1، أعدادًا موجبة كبيرة، وأعدادًا سالبة كبيرة، والمزيد. تتعامل دالتنا البسيطة مع كل هذه الأمور بشكل صحيح. الآن، دعنا نجرب استراتيجية مختلفة لمعرفة ما إذا كان بإمكاننا إيجاد نقطة ضعف.
# دعنا نختبر باستخدام أرقام النقطة العائمة @given(st.floats()) def test_abs_floats_property(x): assert custom_abs(x) >= 0
إذا قمت بتشغيل هذا الأمر، فسوف يعثر Hypothesis بسرعة على حالة فاشلة!
Falsifying example: test_abs_floats_property(x=nan) ... assert custom_abs(nan) >= 0 AssertionError: assert nan >= 0
اكتشف Hypothesis أن دالتنا، عند إعطائها float('nan')
(ليس رقمًا)، تُرجع nan
. التأكيد nan >= 0
خاطئ. لقد وجدنا للتو خطأًا دقيقًا ربما لم نكن لنفكر في اختباره يدويًا. يمكننا إصلاح دالتنا للتعامل مع هذه الحالة، ربما عن طريق إثارة ValueError
أو إرجاع قيمة معينة.
والأفضل من ذلك، ماذا لو كان الخطأ يتعلق بـ float معين جدًا؟ كان من الممكن أن يأخذ تقليص Hypothesis رقمًا فاشلاً كبيرًا ومعقدًا ويقلله إلى أبسط إصدار ممكن لا يزال يؤدي إلى حدوث الخطأ.
قوة الاستراتيجيات: صياغة بيانات الاختبار الخاصة بك
الاستراتيجيات هي قلب Hypothesis. إنها وصفات لتوليد البيانات. تتضمن المكتبة مجموعة كبيرة من الاستراتيجيات المضمنة، ويمكنك دمجها وتخصيصها لإنشاء أي هيكل بيانات تقريبًا يمكنك تخيله.
الاستراتيجيات المضمنة الشائعة
- رقمي:
st.integers(min_value=0, max_value=1000)
: يولد أعدادًا صحيحة، اختياريًا ضمن نطاق معين.st.floats(min_value=0.0, max_value=1.0, allow_nan=False, allow_infinity=False)
: يولد أرقامًا عائمة، مع تحكم دقيق في القيم الخاصة.st.fractions()
,st.decimals()
- نص:
st.text(min_size=1, max_size=50)
: يولد سلاسل unicode بطول معين.st.text(alphabet='abcdef0123456789')
: يولد سلاسل من مجموعة أحرف معينة (مثل رموز hex).st.characters()
: يولد أحرفًا فردية.
- المجموعات:
st.lists(st.integers(), min_size=1)
: يولد قوائم يكون فيها كل عنصر عددًا صحيحًا. لاحظ كيف نمرر استراتيجية أخرى كوسيطة! يسمى هذا التأليف.st.tuples(st.text(), st.booleans())
: يولد مجموعات ثنائية ذات هيكل ثابت.st.sets(st.integers())
st.dictionaries(keys=st.text(), values=st.integers())
: يولد قواميس ذات أنواع مفاتيح وقيم محددة.
- زمني:
st.dates()
,st.times()
,st.datetimes()
,st.timedeltas()
. يمكن جعل هذه الإجراءات حساسة للمنطقة الزمنية.
- متنوع:
st.booleans()
: يولدTrue
أوFalse
.st.just('constant_value')
: يولد دائمًا نفس القيمة الفردية. مفيد لتأليف استراتيجيات معقدة.st.one_of(st.integers(), st.text())
: يولد قيمة من إحدى الاستراتيجيات المقدمة.st.none()
: يولد فقطNone
.
الجمع بين الاستراتيجيات وتحويلها
تأتي القوة الحقيقية لـ Hypothesis من قدرتها على بناء استراتيجيات معقدة من استراتيجيات أبسط.
باستخدام .map()
تتيح لك طريقة .map()
أخذ قيمة من استراتيجية واحدة وتحويلها إلى شيء آخر. هذا مثالي لإنشاء كائنات من فئاتك المخصصة.
# فئة بيانات بسيطة from dataclasses import dataclass @dataclass class User: user_id: int username: str # استراتيجية لتوليد كائنات المستخدم user_strategy = st.builds( User, user_id=st.integers(min_value=1), username=st.text(min_size=3, alphabet='abcdefghijklmnopqrstuvwxyz') ) @given(user=user_strategy) def test_user_creation(user): assert isinstance(user, User) assert user.user_id > 0 assert user.username.isalpha()
باستخدام .filter()
و assume()
في بعض الأحيان، تحتاج إلى رفض قيم معينة تم إنشاؤها. على سبيل المثال، قد تحتاج إلى قائمة بالأعداد الصحيحة التي لا يساوي مجموعها صفرًا. يمكنك استخدام .filter()
:
st.lists(st.integers()).filter(lambda x: sum(x) != 0)
ومع ذلك، يمكن أن يكون استخدام .filter()
غير فعال. إذا كان الشرط خاطئًا بشكل متكرر، فقد يقضي Hypothesis وقتًا طويلاً في محاولة إنشاء مثال صالح. غالبًا ما يكون النهج الأفضل هو استخدام assume()
داخل دالة الاختبار الخاصة بك:
from hypothesis import assume @given(st.lists(st.integers())) def test_something_with_non_zero_sum_list(numbers): assume(sum(numbers) != 0) # ... منطق الاختبار الخاص بك هنا ...
يخبر assume()
Hypothesis: "إذا لم يتم استيفاء هذا الشرط، فتجاهل هذا المثال وحاول مثالاً جديدًا." إنها طريقة مباشرة وغالبًا ما تكون أكثر فعالية لتقييد بيانات الاختبار الخاصة بك.
باستخدام st.composite()
لتوليد بيانات معقدة حقًا حيث تعتمد قيمة تم إنشاؤها على قيمة أخرى، فإن st.composite()
هي الأداة التي تحتاجها. يسمح لك بكتابة دالة تأخذ دالة draw
خاصة كوسيطة، والتي يمكنك استخدامها لسحب القيم من استراتيجيات أخرى خطوة بخطوة.
مثال كلاسيكي هو إنشاء قائمة وفهرس صالح في تلك القائمة.
@st.composite def list_and_index(draw): # أولاً، ارسم قائمة غير فارغة my_list = draw(st.lists(st.integers(), min_size=1)) # ثم، ارسم فهرسًا مضمونًا ليكون صالحًا لهذه القائمة index = draw(st.integers(min_value=0, max_value=len(my_list) - 1)) return (my_list, index) @given(data=list_and_index()) def test_list_access(data): my_list, index = data # يضمن هذا الوصول أن يكون آمنًا بسبب كيفية بناء الإستراتيجية element = my_list[index] assert element is not None # تأكيد بسيط
Hypothesis في العمل: سيناريوهات العالم الحقيقي
دعنا نطبق هذه المفاهيم على مشاكل أكثر واقعية يواجهها مطورو البرامج كل يوم.
السيناريو 1: اختبار دالة تسلسل البيانات
تخيل دالة تقوم بتسلسل ملف تعريف المستخدم (قاموسًا) إلى سلسلة آمنة بعنوان URL وأخرى تقوم بإلغاء تسلسلها. الخاصية الرئيسية هي أن العملية يجب أن تكون قابلة للعكس تمامًا.
import json import base64 def serialize_profile(data: dict) -> str: """يقوم بتسلسل قاموس إلى سلسلة base64 آمنة لعنوان URL.""" json_string = json.dumps(data) return base64.urlsafe_b64encode(json_string.encode('utf-8')).decode('utf-8') def deserialize_profile(encoded_str: str) -> dict: """يقوم بإلغاء تسلسل سلسلة مرة أخرى إلى قاموس.""" json_string = base64.urlsafe_b64decode(encoded_str.encode('utf-8')).decode('utf-8') return json.loads(json_string) # الآن للاختبار # نحتاج إلى استراتيجية تنشئ قواميس متوافقة مع JSON json_dictionaries = st.dictionaries( keys=st.text(), values=st.recursive(st.none() | st.booleans() | st.floats(allow_nan=False) | st.text(), lambda children: st.lists(children) | st.dictionaries(st.text(), children), max_leaves=10) ) @given(profile=json_dictionaries) def test_serialization_roundtrip(profile): """الخاصية: يجب أن يؤدي إلغاء تسلسل ملف تعريف مشفر إلى إرجاع ملف التعريف الأصلي.""" encoded = serialize_profile(profile) decoded = deserialize_profile(encoded) assert profile == decoded
سوف يضرب هذا الاختبار الواحد الدوال الخاصة بنا بمجموعة كبيرة من البيانات: قواميس فارغة، وقواميس تحتوي على قوائم متداخلة، وقواميس تحتوي على أحرف unicode، وقواميس تحتوي على مفاتيح غريبة، والمزيد. إنه أكثر شمولاً بكثير من كتابة بعض الأمثلة اليدوية.
السيناريو 2: اختبار خوارزمية الفرز
دعنا نعود إلى مثال الفرز الخاص بنا. إليك الطريقة التي يمكنك بها اختبار الخصائص التي حددناها مسبقًا.
from collections import Counter def my_buggy_sort(numbers): # دعنا نقدم خطأًا دقيقًا: إنه يسقط التكرارات return sorted(list(set(numbers))) @given(st.lists(st.integers())) def test_sorting_properties(numbers): sorted_list = my_buggy_sort(numbers) # الخاصية 1: المخرجات مرتبة for i in range(len(sorted_list) - 1): assert sorted_list[i] <= sorted_list[i+1] # الخاصية 2: العناصر هي نفسها (سيؤدي هذا إلى إيجاد الخطأ) assert Counter(numbers) == Counter(sorted_list) # الخاصية 3: الوظيفة ذاتية التكرار assert my_buggy_sort(sorted_list) == sorted_list
عند تشغيل هذا الاختبار، سيجد Hypothesis بسرعة مثالًا فاشلاً للخاصية 2، مثل numbers=[0, 0]
. تُرجع دالتنا [0]
، و Counter([0, 0])
لا يساوي Counter([0])
. سيضمن المُقلّص أن يكون المثال الفاشل بسيطًا قدر الإمكان، مما يجعل سبب الخطأ واضحًا على الفور.
السيناريو 3: الاختبار ذو الحالة
بالنسبة للكائنات التي تتغير حالتها الداخلية بمرور الوقت (مثل اتصال قاعدة البيانات، أو سلة التسوق، أو ذاكرة التخزين المؤقت)، قد يكون العثور على الأخطاء أمرًا صعبًا بشكل لا يصدق. قد يلزم تسلسل معين من العمليات لإثارة خطأ. يوفر Hypothesis `RuleBasedStateMachine` لهذا الغرض على وجه التحديد.
تخيل واجهة برمجة تطبيقات بسيطة لمخزن قيمة مفتاح في الذاكرة:
class SimpleKeyValueStore: def __init__(self): self._data = {} def set(self, key, value): self._data[key] = value def get(self, key): return self._data.get(key) def delete(self, key): if key in self._data: del self._data[key] def size(self): return len(self._data)
يمكننا نمذجة سلوكه واختباره باستخدام آلة الحالة:
from hypothesis.stateful import RuleBasedStateMachine, rule, Bundle class KeyValueStoreMachine(RuleBasedStateMachine): def __init__(self): super().__init__() self.model = {} self.sut = SimpleKeyValueStore() # يتم استخدام Bundle() لتمرير البيانات بين القواعد keys = Bundle('keys') @rule(target=keys, key=st.text(), value=st.integers()) def set_key(self, key, value): self.model[key] = value self.sut.set(key, value) return key @rule(key=keys) def delete_key(self, key): del self.model[key] self.sut.delete(key) @rule(key=st.text()) def get_key(self, key): model_val = self.model.get(key) sut_val = self.sut.get(key) assert model_val == sut_val @rule() def check_size(self): assert len(self.model) == self.sut.size() # لتشغيل الاختبار، ما عليك سوى إنشاء فئة فرعية من الجهاز و unittest.TestCase # في pytest، يمكنك ببساطة تعيين الاختبار إلى فئة الجهاز TestKeyValueStore = KeyValueStoreMachine.TestCase
سيقوم Hypothesis الآن بتنفيذ تسلسلات عشوائية من عمليات set_key
و delete_key
و get_key
و check_size
، في محاولة بلا هوادة للعثور على تسلسل يتسبب في فشل أحد التأكيدات. سيتحقق مما إذا كان الحصول على مفتاح محذوف يتصرف بشكل صحيح، وإذا كان الحجم متوافقًا بعد عمليات التعيين والحذف المتعددة، والعديد من السيناريوهات الأخرى التي قد لا تفكر في اختبارها يدويًا.
أفضل الممارسات والنصائح المتقدمة
- قاعدة بيانات الأمثلة: Hypothesis ذكي. عندما يجد خطأً، فإنه يحفظ المثال الفاشل في دليل محلي (
.hypothesis/
). في المرة التالية التي تقوم فيها بتشغيل الاختبارات الخاصة بك، سيعيد تشغيل هذا المثال الفاشل أولاً، مما يمنحك ملاحظات فورية بأن الخطأ لا يزال موجودًا. بمجرد إصلاحه، لم يعد المثال يعاد تشغيله. - التحكم في تنفيذ الاختبار باستخدام
@settings
: يمكنك التحكم في العديد من جوانب تشغيل الاختبار باستخدام المزخرف@settings
. يمكنك زيادة عدد الأمثلة، وتعيين موعد نهائي للمدة التي يمكن أن يستغرقها مثال واحد (لالتقاط الحلقات اللانهائية)، وإيقاف تشغيل بعض الفحوصات الصحية.@settings(max_examples=500, deadline=1000) # قم بتشغيل 500 مثال، موعد نهائي لمدة ثانية واحدة @given(...) ...
- إعادة إنتاج حالات الفشل: يطبع كل تشغيل Hypothesis قيمة البذور (على سبيل المثال،
@reproduce_failure('version', 'seed')
). إذا وجد خادم CI خطأ لا يمكنك إعادة إنتاجه محليًا، فيمكنك استخدام هذا المزخرف مع البذرة المتوفرة لإجبار Hypothesis على تشغيل نفس التسلسل بالضبط من الأمثلة. - التكامل مع CI/CD: Hypothesis هو الحل الأمثل لأي خط أنابيب تكامل مستمر. إن قدرته على العثور على الأخطاء الغامضة قبل وصولها إلى الإنتاج تجعلها شبكة أمان لا تقدر بثمن.
تحول العقلية: التفكير في الخصائص
إن تبني Hypothesis هو أكثر من مجرد تعلم مكتبة جديدة؛ يتعلق الأمر بتبني طريقة جديدة للتفكير في صحة التعليمات البرمجية الخاصة بك. بدلاً من طرح السؤال، "ما هي المدخلات التي يجب أن أختبرها؟"، تبدأ في طرح السؤال، "ما هي الحقائق العالمية حول هذه التعليمات البرمجية؟"
فيما يلي بعض الأسئلة لإرشادك عند محاولة تحديد الخصائص:
- هل توجد عملية عكسية؟ (على سبيل المثال، تسلسل/إلغاء التسلسل، التشفير/فك التشفير، الضغط/فك الضغط). الخاصية هي أن إجراء العملية وعكسها يجب أن ينتج الإدخال الأصلي.
- هل العملية ذاتية التكرار؟ (على سبيل المثال،
abs(abs(x)) == abs(x)
). يجب أن يؤدي تطبيق الوظيفة أكثر من مرة إلى إنتاج نفس النتيجة مثل تطبيقها مرة واحدة. - هل هناك طريقة مختلفة وأبسط لحساب نفس النتيجة؟ يمكنك اختبار أن وظيفتك المعقدة والمحسّنة تنتج نفس المخرجات مثل إصدار بسيط وصحيح بشكل واضح (على سبيل المثال، اختبار الفرز الفاخر الخاص بك مقابل Python's built-in
sorted()
). - ما الذي يجب أن يكون صحيحًا دائمًا بشأن المخرجات؟ (على سبيل المثال، يجب أن تحتوي مخرجات دالة `find_prime_factors` فقط على الأعداد الأولية، ويجب أن يساوي حاصل ضربها الإدخال).
- كيف تتغير الحالة؟ (للاختبار ذي الحالة) ما هي الثوابت التي يجب الحفاظ عليها بعد أي عملية صالحة؟ (على سبيل المثال، لا يمكن أن يكون عدد العناصر الموجودة في سلة التسوق سالبًا أبدًا).
الخلاصة: مستوى جديد من الثقة
الاختبار القائم على الخصائص باستخدام Hypothesis لا يحل محل الاختبار القائم على الأمثلة. لا تزال بحاجة إلى اختبارات محددة مكتوبة بخط اليد لمنطق العمل الحاسم والمتطلبات المفهومة جيدًا (على سبيل المثال، "يجب أن يرى المستخدم من البلد X السعر Y").
ما يوفره Hypothesis هو طريقة آلية قوية لاستكشاف سلوك التعليمات البرمجية الخاصة بك والحماية من الحالات الطرفية غير المتوقعة. إنه بمثابة شريك لا يكل، يقوم بإنشاء الآلاف من الاختبارات التي تكون أكثر تنوعًا وخبثًا مما يمكن لأي شخص كتابته بشكل واقعي. من خلال تحديد الخصائص الأساسية للتعليمات البرمجية الخاصة بك، فإنك تنشئ مواصفات قوية يمكن لـ Hypothesis الاختبار عليها، مما يمنحك مستوى جديدًا من الثقة في برنامجك.
في المرة القادمة التي تكتب فيها دالة، خذ لحظة للتفكير فيما وراء الأمثلة. اسأل نفسك، "ما هي القواعد؟ ما الذي يجب أن يكون صحيحًا دائمًا؟" ثم، دع Hypothesis يقوم بالعمل الشاق لمحاولة كسرها. ستتفاجأ بما تجده، وستكون التعليمات البرمجية الخاصة بك أفضل لذلك.